返回文章列表

Set、Sorted Set 与 GEO

系统梳理 Redis Set、Sorted Set 与 GEO 的使用场景、集合运算风险、排行榜、延迟任务、double score 精度、跳表实现和 Go 使用边界。

第 6 章:Set、Sorted Set 与 GEO

1. 本章定位

本章解决三类问题:

  • Set:某个元素是否属于一个集合,以及多个集合之间有什么关系。
  • Sorted Set:既要保证成员唯一,又要按照分值持续排序。
  • GEO:存储经纬度,并完成距离计算和附近位置检索。

三者看似应用差异很大,底层却存在联系:Set 侧重唯一性和集合运算;Sorted Set 在唯一成员之上增加顺序;GEO 又建立在 Sorted Set 之上,将地理坐标编码到 score 中。

截至 2026 年 6 月,本章以 Redis Open Source 8.8 为技术基线,同时说明旧版本中常见的实现差异。(Redis)


2. 学习目标

学完本章,应当能够:

  1. 使用 Set 实现去重、标签、权限、共同关注和抽奖。
  2. 评估 SINTERSUNIONSDIFF 对 Redis 延迟的影响。
  3. 使用 Sorted Set 实现排行榜、权重队列、滑动时间窗口和延迟任务。
  4. 正确解释重复 score、成员唯一性和字典序规则。
  5. 理解 double score 的精度边界,避免时间戳和复合分值错误。
  6. 解释 Sorted Set 为什么同时使用字典和跳表。
  7. 说明 GEO 的存储方式、距离误差和适用边界。
  8. 使用 go-redis/v9 编写具备超时、错误处理和并发安全意识的代码。

2.1 本章边界与跳转

本章是 Set、Sorted Set 与 GEO 的主章节。Sorted Set 延迟任务这里只讲数据结构建模和基础领取风险;生产级延迟任务的认领、确认、重试和幂等状态机见第 19 章,Streams 消费者组方案见[第 8 章](/blog/tech/Redis/08.PubSub、List 队列与 Streams/);跳表、dict、intset 等底层结构见[第 11 章](/blog/tech/Redis/11.Redis 核心对象与底层数据结构/);大集合运算造成的性能问题见[第 12 章](/blog/tech/Redis/12.Redis 高性能原理/)和第 20 章

3. 核心概念

3.1 Set:无序且成员唯一

Redis Set 是一个无序的唯一字符串集合。同一成员重复执行 SADD 不会产生两份数据,命令返回值可以区分本次是否真的新增了成员。(Redis)

典型建模如下:

业务KeyMember
用户关注列表follow:user:1001被关注用户 ID
文章标签关系article:88:tags标签 ID
标签反向索引tag:redis:articles文章 ID
角色成员role:admin:users用户 ID
活动候选人lottery:2026:users用户 ID
请求去重dedup:order:20260621请求唯一 ID

Set 的优势不只是去重,还包括三类集合运算:

  • 交集:同时属于多个集合的成员,例如共同关注。
  • 并集:至少属于一个集合的成员,例如合并多个人群。
  • 差集:属于 A 但不属于 B 的成员,例如“我关注但你没有关注的人”。

3.2 Set 不保证业务顺序

Set 没有插入顺序、更新时间顺序或稳定返回顺序。即使某次 SMEMBERS 的结果看起来固定,也不能把这种偶然顺序写进业务协议。

需要顺序时应改用:

  • List:按插入顺序或队列顺序。
  • Sorted Set:按业务分值、时间或优先级排序。
  • Stream:需要消息 ID、消费者组和确认机制。

3.3 大集合运算为什么危险

SINTERSUNIONSDIFF 都需要访问大量成员。其风险不仅是算法复杂度,还包括:

  1. Redis 执行命令期间占用命令执行线程。
  2. 需要为结果分配内存。
  3. 结果要编码成 RESP 响应。
  4. 大结果通过网络发送,占用带宽和客户端输出缓冲区。
  5. 客户端还需要分配内存反序列化。

官方复杂度中,SINTER 最坏可达到 O(N × M),其中 N 是最小集合的基数,M 是参与运算的集合数;SUNIONSDIFF 与输入集合成员总量相关。(Redis)

因此,“共同关注只有十几个结果”并不意味着运算便宜。Redis 仍可能扫描一个拥有数百万成员的集合。

3.4 Sorted Set:唯一成员加可排序 score

Sorted Set,简称 ZSet,由两部分组成:

  • member:唯一字符串。
  • score:用于排序的 64 位 double 浮点数。

同一 member 再次执行 ZADD,默认会更新其 score,而不是创建重复成员。不同 member 可以拥有相同 score。

默认排序规则为:

  1. score 从小到大。
  2. score 相同时,按 member 的二进制字节序排序。
  3. 使用反向范围时,score 从大到小,相同 score 按反向字典序排序。(Redis)

这意味着“积分相同就按先到先得”不会自然成立。要实现确定的第二排序字段,必须显式设计。

3.5 Sorted Set 的四类核心场景

1. 排行榜

key    = leaderboard:game:2026-06
member = userID
score  = 用户积分

使用 ZINCRBY 原子增加积分,用 ZRANGE ... REV 查询高分用户,用 ZREVRANK 查询用户从高到低的名次。

2. 权重队列

key    = queue:image:priority
member = taskID
score  = 优先级

如果较小 score 代表更高优先级,可用 ZPOPMIN;如果较大 score 优先,则使用 ZPOPMAX 或转换评分方向。

权重队列还要处理饥饿问题:高优先级任务持续进入时,低优先级任务可能长期得不到处理。常见改进是引入等待时间加权或分级队列。

3. 时间窗口

key    = request:user:1001
member = 唯一请求 ID
score  = 请求发生时间戳

通过 score 范围查询最近一分钟的请求,并定期删除窗口之外的数据。这适用于精确滑动日志限流,但高频请求下内存成本较高。

4. 延迟调度

key    = delay:{email}:ready
member = taskID
score  = 计划执行时间

消费者查找 score <= 当前时间 的任务。但“查找后删除”是两个步骤,多消费者并发时会产生竞态,必须通过 Lua、事务或其他原子领取机制修正。

3.6 score 精度边界

Sorted Set 的 score 是 64 位 double。整数只有在 [-2^53, 2^53] 范围内才能被精确表示,即大约正负 9.007 × 10^15。超过这一范围后,相邻整数可能映射为同一个浮点值。(Redis)

工程上应注意:

  • 当前 Unix 毫秒时间戳约为 10^12,可以精确表示。
  • Unix 微秒时间戳约为 10^15,仍在边界内,但剩余空间有限。
  • Unix 纳秒时间戳约为 10^18,不能作为精确整数 score。
  • 不要随意通过“时间戳加一个极小小数”实现第二排序字段。
  • 分数、时间、权重最好统一成有明确量纲的整数,再转换为 float64
  • 若必须构造复合 score,应证明最大值始终小于 2^53

3.7 GEO:建立在 Sorted Set 上的地理索引

Redis GEO 用于:

  • 存储经纬度。
  • 计算两个成员之间的直线距离。
  • 查询给定圆形或矩形范围内的成员。

它适合“附近门店”“附近车辆”“附近骑行站点”等简单场景,但不是完整 GIS 系统,不擅长:

  • 多边形包含判断。
  • 道路导航距离。
  • 海拔和三维空间。
  • 行政区边界。
  • 复杂地理条件组合。
  • 极高精度测绘。

官方也明确区分了基础 GEO 数据类型与 Redis Search 中更丰富的地理检索能力。(Redis)


4. 命令与 Go 使用方法

4.1 Set 常用命令

# 添加关注,重复成员不会重复存储
SADD follow:user:1001 user:2001 user:2002 user:2003

# 取消关注
SREM follow:user:1001 user:2002

# 判断是否关注
SISMEMBER follow:user:1001 user:2001

# 共同关注
SINTER follow:user:1001 follow:user:1002

# 两人的全部关注对象
SUNION follow:user:1001 follow:user:1002

# user:1001 关注、但 user:1002 未关注的人
SDIFF follow:user:1001 follow:user:1002

# 随机抽取但不删除
SRANDMEMBER lottery:2026:users 10

# 随机抽取并删除,适用于不重复中奖
SPOP lottery:2026:users 10

SPOP 只是从 Redis 集合中删除成员,并不能单独构成完整抽奖系统。真实抽奖还要处理资格快照、审计记录、并发开奖、结果持久化和失败重试。

4.2 Sorted Set 常用命令

# 添加或更新积分
ZADD leaderboard:game:2026-06 1500 user:1001
ZADD leaderboard:game:2026-06 1300 user:1002

# 原子增加积分
ZINCRBY leaderboard:game:2026-06 50 user:1001

# 从低分到高分查询
ZRANGE leaderboard:game:2026-06 0 9 WITHSCORES

# Redis 6.2+:从高分到低分查询
ZRANGE leaderboard:game:2026-06 0 9 REV WITHSCORES

# 从低到高的排名,排名从 0 开始
ZRANK leaderboard:game:2026-06 user:1001

# 从高到低的排名
ZREVRANK leaderboard:game:2026-06 user:1001

# 弹出最低分成员
ZPOPMIN queue:image:priority 1

# 查询当前已到执行时间的任务;仅查询,不是安全领取
ZRANGE delay:{email}:ready -inf 1782000000000 BYSCORE LIMIT 0 10

ZRANGE 的区间包含首尾位置;按排名查询的复杂度为 O(log N + M),其中 M 是返回数量。较大的 offset 仍可能产生明显遍历成本。(Redis)

4.3 GEO 常用命令

# 注意参数顺序是经度、纬度
GEOADD geo:shops 116.397128 39.916527 shop:1
GEOADD geo:shops 116.405285 39.904989 shop:2

# 计算两点距离
GEODIST geo:shops shop:1 shop:2 km

# Redis 6.2+:以经纬度为中心查询 5 公里内的位置
GEOSEARCH geo:shops \
  FROMLONLAT 116.397128 39.916527 \
  BYRADIUS 5 km \
  ASC COUNT 20 WITHDIST

# GEO 实际存储为 Sorted Set,可使用 ZREM 删除
ZREM geo:shops shop:2

GEOSEARCH 支持按圆形或矩形查询。COUNT ... ANY 允许 Redis 找到足够结果后提前停止,可能更快,但不保证返回的就是全局最近成员。GEORADIUS 系列命令自 Redis 6.2 起已被标记为废弃方向,新代码优先使用 GEOSEARCHGEOSEARCHSTORE。(Redis)

4.4 Go 客户端基础配置

package redisexample

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func NewRedisClient() *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr:         "127.0.0.1:6379",
		DialTimeout:  2 * time.Second,
		ReadTimeout:  800 * time.Millisecond,
		WriteTimeout: 800 * time.Millisecond,
		PoolTimeout:  1 * time.Second,
	})
}

redis.Client 内部管理连接池,可以被多个 goroutine 并发复用。不要为每个请求创建一个 Client;应在进程级复用,并在进程退出时调用 Close

网络读写超时是客户端级上限,每个业务调用仍应使用更具体的 context.WithTimeout

4.5 Go 实现共同关注

func CommonFollows(
	parent context.Context,
	rdb *redis.Client,
	userA, userB string,
) ([]string, error) {
	ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
	defer cancel()

	keyA := "follow:" + userA
	keyB := "follow:" + userB

	users, err := rdb.SInter(ctx, keyA, keyB).Result()
	if err != nil {
		return nil, fmt.Errorf("SINTER %s %s: %w", keyA, keyB, err)
	}
	return users, nil
}

注意事项:

  • 没有共同关注时通常返回空切片,不是 redis.Nil
  • 两个集合很大时,这段代码虽然简单,却可能形成慢命令。
  • Redis Cluster 中两个 Key 必须位于同一 Slot 才能直接执行多 Key 集合运算。
  • 不能仅通过缩短 Go 超时解决服务器阻塞;客户端放弃等待后,Redis 上的命令可能仍在执行。

4.6 Go 实现排行榜

type RankItem struct {
	UserID string
	Score  float64
	Rank   int64 // 对外按 1 开始
}

func AddLeaderboardScore(
	parent context.Context,
	rdb *redis.Client,
	key, userID string,
	delta float64,
) (float64, error) {
	ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
	defer cancel()

	score, err := rdb.ZIncrBy(ctx, key, delta, userID).Result()
	if err != nil {
		return 0, fmt.Errorf("increment leaderboard score: %w", err)
	}
	return score, nil
}

func TopLeaderboard(
	parent context.Context,
	rdb *redis.Client,
	key string,
	limit int64,
) ([]RankItem, error) {
	if limit <= 0 || limit > 1000 {
		return nil, fmt.Errorf("invalid limit: %d", limit)
	}

	ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
	defer cancel()

	values, err := rdb.ZRevRangeWithScores(ctx, key, 0, limit-1).Result()
	if err != nil {
		return nil, fmt.Errorf("query leaderboard: %w", err)
	}

	result := make([]RankItem, 0, len(values))
	for i, value := range values {
		userID, ok := value.Member.(string)
		if !ok {
			return nil, fmt.Errorf("unexpected member type %T", value.Member)
		}
		result = append(result, RankItem{
			UserID: userID,
			Score:  value.Score,
			Rank:   int64(i) + 1,
		})
	}
	return result, nil
}

func UserLeaderboardRank(
	parent context.Context,
	rdb *redis.Client,
	key, userID string,
) (rank int64, found bool, err error) {
	ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
	defer cancel()

	zeroBased, err := rdb.ZRevRank(ctx, key, userID).Result()
	switch {
	case err == nil:
		return zeroBased + 1, true, nil
	case errors.Is(err, redis.Nil):
		return 0, false, nil
	default:
		return 0, false, fmt.Errorf("query user rank: %w", err)
	}
}

并发调用 ZINCRBY 时,每条命令在 Redis 端是原子的,不会发生普通的“读旧值、覆盖新值”问题。但业务上的积分变更还应具备事件 ID 或幂等标识,否则客户端超时重试可能把同一积分增加两次。

4.7 Go 实现可靠性更高的延迟领取

错误实现通常是:

ZRANGE 找到到期任务
ZREM   删除任务
执行任务

多个消费者可能同时读到同一个任务。更稳妥的方式是使用短 Lua 脚本,原子地将任务从 ready 集合移动到带租约的 processing 集合。

var claimDueTaskScript = redis.NewScript(`
local ids = redis.call(
    'ZRANGE', KEYS[1], '-inf', ARGV[1],
    'BYSCORE', 'LIMIT', 0, 1
)

if #ids == 0 then
    return nil
end

local id = ids[1]
if redis.call('ZREM', KEYS[1], id) == 1 then
    redis.call('ZADD', KEYS[2], ARGV[2], id)
    return id
end

return nil
`)

func EnqueueDelayedTask(
	parent context.Context,
	rdb *redis.Client,
	readyKey, taskID string,
	runAt time.Time,
) error {
	ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
	defer cancel()

	err := rdb.ZAdd(ctx, readyKey, redis.Z{
		Score:  float64(runAt.UnixMilli()),
		Member: taskID,
	}).Err()
	if err != nil {
		return fmt.Errorf("enqueue delayed task: %w", err)
	}
	return nil
}

func ClaimDueTask(
	parent context.Context,
	rdb *redis.Client,
	readyKey, processingKey string,
	now time.Time,
	lease time.Duration,
) (taskID string, found bool, err error) {
	ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
	defer cancel()

	result, err := claimDueTaskScript.Run(
		ctx,
		rdb,
		[]string{readyKey, processingKey},
		now.UnixMilli(),
		now.Add(lease).UnixMilli(),
	).Text()

	switch {
	case err == nil:
		return result, true, nil
	case errors.Is(err, redis.Nil):
		return "", false, nil
	default:
		return "", false, fmt.Errorf("claim delayed task: %w", err)
	}
}

func AckDelayedTask(
	parent context.Context,
	rdb *redis.Client,
	processingKey, taskID string,
) error {
	ctx, cancel := context.WithTimeout(parent, 500*time.Millisecond)
	defer cancel()

	removed, err := rdb.ZRem(ctx, processingKey, taskID).Result()
	if err != nil {
		return fmt.Errorf("ack delayed task: %w", err)
	}
	if removed == 0 {
		return fmt.Errorf("task %s is no longer in processing set", taskID)
	}
	return nil
}

这个方案还需要补齐以下机制:

  1. 租约回收:扫描 processingscore <= 当前时间 的任务,将它们重新放回 ready
  2. 业务幂等:消费者可能处理成功但 ACK 超时,任务随后被重新执行。
  3. 失败重试:记录重试次数并进行指数退避,超过阈值进入死信区。
  4. 负载控制:空队列时退避轮询,避免大量消费者持续查询。
  5. 原子入队:若任务正文和调度索引分别存储,应通过 Lua 或事务共同写入。
  6. 集群同槽:可使用 delay:{email}:readydelay:{email}:processing,花括号中的 Hash Tag 保证两个 Key 位于同一 Slot。

Lua 脚本在 Redis 中作为一个原子操作执行,因此脚本必须保持短小;长循环或一次领取大量任务会阻塞其他命令。(Redis)

该方案接近至少一次投递,不能宣称恰好一次。恰好一次通常需要业务数据库的唯一约束、状态机或事务性消息共同参与。

4.8 Go 查询附近位置

func NearbyShops(
	parent context.Context,
	rdb *redis.Client,
	key string,
	longitude, latitude, radiusKM float64,
) ([]string, error) {
	ctx, cancel := context.WithTimeout(parent, 800*time.Millisecond)
	defer cancel()

	shops, err := rdb.GeoSearch(ctx, key, &redis.GeoSearchQuery{
		Longitude:  longitude,
		Latitude:   latitude,
		Radius:     radiusKM,
		RadiusUnit: "km",
	}).Result()
	if err != nil {
		return nil, fmt.Errorf("search nearby shops: %w", err)
	}
	return shops, nil
}

官方文档中的 Go API 同样使用 GeoAddGeoLocationGeoSearchQuery。(Redis)


5. 典型业务场景

场景适用性数据量要求一致性要求主要性能风险可替代方案
请求 ID 去重适合短周期精确去重中小规模,按时间分桶并设置 TTL通常要求较强的原子判重Set 无限增长、单 Key 过大String SET NX、数据库唯一键、布隆过滤器
用户标签适合标签成员和反向索引标签分布不能过度倾斜可接受短暂最终一致时容易实现热门标签形成 BigKey/HotKey数据库关联表、Search Tag 索引
权限成员适合角色成员集合权限集合应相对可控高权限场景不能只依赖过期副本读取主从延迟导致撤权不及时关系数据库、策略引擎、本地短缓存
共同关注集合较小时合适应限制单用户关注上限一般可接受短暂不一致大集合交集阻塞、Cluster 跨槽离线预计算、图数据库、数据库查询
抽奖候选人适合候选池快照候选集合需可放入内存结果必须可审计SPOP 后业务写入失败数据库锁定、预生成随机顺序
实时排行榜非常适合单榜从几千到数百万需容量评估一般为最终一致;奖励结算需二次校验热榜 Key、深分页、大范围读取数据库窗口函数、离线数仓
权重任务队列适合简单优先级队列队列长度应受控接受至少一次时较合适饥饿、任务丢失、热点轮询Kafka、RabbitMQ、Redis Streams
延迟任务适合中小规模调度应按业务或时间分片需要幂等、租约和回收轮询惊群、超时重试、故障转移专用调度系统、消息队列延迟插件
滑动时间窗口适合精确事件窗口单用户窗口事件不能过密通常要求原子计数与裁剪每次请求一个 member,内存较大令牌桶、时间分桶计数
附近的人/门店适合简单圆形或矩形附近查询单 Key 过大时需区域分片多数场景接受最终一致热区域、返回结果过多、坐标错误Redis Search、PostGIS、专业 GIS

6. 底层实现

6.1 Set 的内部编码

经典面试资料常把 Set 概括为:

  • 全整数、小集合:intset
  • 其他情况:hashtable

这一回答对不少旧版本和经典实现是有意义的,但对当前版本不够完整。Redis 8.8 的 Set 源码包含:

  • intset
  • listpack
  • hashtable

三类编码分支。(GitHub)

intset

intset 是紧凑的有序整数数组:

  • 只适合能够解析为整数的成员。
  • 根据整数范围选择较小的整数宽度。
  • 通过连续内存降低指针和哈希桶开销。
  • 查找可利用有序数组,但插入中间位置可能需要移动数据。

它适合小规模整数集合,核心取舍是:用较高的局部操作成本换取较低内存占用和更好的缓存局部性

listpack

listpack 是紧凑连续编码:

  • 适合成员较少、成员长度较短的 Set。
  • 减少哈希表桶、指针和独立内存分配的开销。
  • 查找通常需要线性扫描,但因为集合很小且内存连续,实际可能较快。
  • 随着成员增加或成员过长,会转换成更适合大集合的编码。

当前配置中可通过 set-max-listpack-entriesset-max-listpack-value 控制相关阈值;具体默认值可能随版本和发行配置变化,不能把阈值写死为业务假设。(GitHub)

hashtable

大集合通常采用哈希表:

  • SISMEMBER 平均接近 O(1)
  • 增删成员平均接近 O(1)
  • 内存成本高于连续紧凑结构。
  • 扩容和渐进式 rehash 会带来额外工作。
  • 随机访问能力强,适合大规模成员集合。

官方命令复杂度把 SADD 表述为每个元素 O(1),这是面向命令使用者的典型复杂度;分析具体编码时,还应考虑紧凑编码的小规模线性扫描和转换成本。(Redis)

6.2 Set 集合运算算法

SINTER

Redis通常优先从较小的集合出发,逐个检查其成员是否存在于其他集合中。这样比遍历最大集合更经济。

假设三个集合大小分别为:

A = 100
B = 100 万
C = 500 万

从 A 出发最多只需验证 100 个候选,而不是先遍历 C。但每个候选仍要在其他集合中执行成员检查,集合数量增加也会增加成本。

SUNION

遍历所有输入集合,将成员加入结果集合并自然去重。成本与输入成员总量相关,不仅与最终结果数量相关。

SDIFF

差集对参数顺序敏感:

SDIFF A B C

表示 A - B - C。其成本由第一个集合和后续集合的规模共同决定。因此,不能随意交换 Key 顺序来“优化”而不改变业务语义。

STORE 版本

SINTERSTORESUNIONSTORESDIFFSTORE 把结果存入 Redis,能减少大结果向客户端传输,但不会消除服务器端计算成本,而且会增加写入、内存和复制流量。

只关心交集数量时,应优先考虑 SINTERCARD,避免传回完整成员列表。

6.3 Sorted Set 的紧凑编码与双索引

小型 Sorted Set 可以使用 listpack,降低内存开销;集合扩大后,经典的大集合实现同时维护:

  1. 字典
  2. 跳表

官方 Sorted Set 文档明确描述了其跳表与哈希表组合结构。(Redis)

字典负责什么

字典通常以 member 为 Key,快速定位其 score 或关联节点:

  • 查询成员是否存在:平均 O(1)
  • 获取成员当前 score:平均 O(1)
  • 更新已存在成员前,快速找到旧值。

若只有跳表,根据 member 查找会很困难,因为跳表主要按 (score, member) 排序,调用方只提供 member 时无法高效定位。

跳表负责什么

跳表按照 (score, member) 有序组织节点:

  • 插入、删除:期望 O(log N)
  • 按 score 查询范围:定位起点后顺序遍历。
  • 获取排名:通过层级跨度信息计算。
  • Top N:从高分端反向遍历。
  • 同分成员:再按 member 二进制顺序排列。

为什么不是只用平衡树

平衡树同样可以实现 O(log N),Redis 选择跳表主要因为:

  • 算法和实现相对直接。
  • 范围遍历自然。
  • 通过随机层级维持期望复杂度。
  • 插入和删除涉及的结构调整相对局部。
  • 可以方便维护前后向链表和排名跨度。

这不是“跳表在所有方面都优于红黑树”,而是 Redis 在复杂度、范围查询、实现维护和工程成本之间的选择。

为什么不能只用字典

字典能快速定位 member,却无法高效回答:

  • 分数最高的前 100 名是谁?
  • score 在 [1000, 2000] 之间的成员有哪些?
  • 某个成员排第几?
  • 删除最小 score 的任务是谁?

因此,字典和跳表是两套不同访问路径的组合,代价是更多内存和每次写入同时更新两个索引。

6.4 score 与排序键

Sorted Set 实际按以下组合键排序:

(score, member)

因此:

  • member 唯一。
  • score 可以重复。
  • score 相同时 member 决定顺序。
  • 修改 member 需要删除旧成员再添加新成员。
  • 修改 score 会改变其在跳表中的位置。

实现“同分时先到先得”可考虑:

  1. 保留 score 为业务积分,在 member 前附加可排序序号。
  2. 把业务积分和序号编码为不超过 2^53 的复合整数。
  3. 将同分用户放入另一个按时间排序的结构。
  4. 结算时在数据库中使用明确的第二排序字段。

第一种方式会影响以真实用户 ID 查询,需要维护映射;第二种方式需要严格证明数值范围;第三种方式增加一致性成本。

6.5 GEO 的存储模型

Redis GEO 将经纬度编码为大约 52 位的整数值,并作为 Sorted Set 的 score 存储。经纬度的二进制位交错组合,使空间上相近的位置通常具有相近的编码。(Redis)

查询附近成员时,大致过程为:

  1. 根据中心点和半径计算覆盖区域。
  2. 找到中心网格及相邻网格。
  3. 利用 Sorted Set 的 score 范围筛选候选点。
  4. 对候选点计算实际球面距离。
  5. 过滤掉落在边界框内但不在目标圆形内的点。
  6. 根据需要排序、限制数量并返回。

GEOADD 要求经度在 [-180, 180],纬度在约 [-85.05112878, 85.05112878] 范围内。Redis 的距离模型把地球近似为球体,边缘情况下误差可能达到约 0.5%,因此不适合测绘、计费里程或法律边界判定。(Redis)

6.6 主要命令复杂度

命令典型复杂度关键风险
SADD / SREM每成员平均 O(1)批量参数过多、编码转换
SISMEMBER平均 O(1)超大热 Key
SINTER最坏约 O(N × M)大集合阻塞
SUNION / SDIFFO(输入成员总量)大结果与网络开销
ZADD / ZINCRBY每成员 O(log N)热榜写竞争
ZRANGEO(log N + M)大范围、深分页
ZRANK / ZREVRANKO(log N)高频单榜查询
ZPOPMIN多成员约 O(M log N)弹出后任务处理失败
GEOADDO(log N)热区域频繁更新
GEODISTO(1)模型精度边界
GEOSEARCHO(N + log M)查询区域大、候选点多

其中 NM 在不同命令文档中含义不同,不能脱离命令定义机械比较。(Redis)


7. 高性能、高并发、高可用分析

7.1 高性能

CPU

Set 单成员操作和 Sorted Set 单点操作通常很快,但以下命令可能消耗大量 CPU:

  • 多个大 Set 的交并差。
  • 返回大范围的 ZRANGE
  • 过大区域的 GEOSEARCH
  • 大批量 ZREMZPOPMIN
  • 包含循环的长 Lua 脚本。

客户端超时只能限制调用方等待时间,不能保证已经发送到 Redis 的命令立即停止。

内存

主要内存风险包括:

  • Set 或 Sorted Set 成为 BigKey。
  • Sorted Set 同时维护字典和跳表,元数据成本较高。
  • 时间窗口为每个请求创建唯一 member。
  • 延迟任务长期堆积。
  • GEO 在一个城市级 Key 中存放过多对象。
  • SUNION 等命令临时创建巨大结果。

网络

返回十万个成员时,即使服务器计算很快,网络传输和客户端反序列化也可能成为主要延迟来源。

常见控制方式:

  • 设置分页和最大返回条数。
  • 只需要数量时使用计数命令。
  • 使用 STORE 版本在 Redis 内保存中间结果,但要评估额外内存。
  • 不向普通接口直接暴露“返回全部成员”能力。

Pipeline

Pipeline 能减少多条独立命令的网络往返,但不会降低单条 SINTERGEOSEARCH 的算法复杂度,也不会使多条命令自动具备原子性。(Redis)

7.2 高并发

原子性

单条 SADDZINCRBYZPOPMIN 在单个 Redis 节点上是原子的,但业务流程通常不是单条命令:

领取任务 → 执行业务 → 写数据库 → ACK

这整个流程不会因为使用 Redis 就自动原子化。

热点

以下 Key 容易成为 HotKey:

  • 全服总榜。
  • 超级用户的关注集合。
  • 全国门店 GEO Key。
  • 所有延迟任务共用一个 ZSet。
  • 热门标签的成员集合。

可选的分散方式包括:

  • 按赛季、地区、租户或业务类型分 Key。
  • 维护分区榜和异步总榜。
  • GEO 按区域网格分片。
  • 延迟任务按业务、时间桶或分区号拆分。
  • 限制单集合成员规模。

但分片后,跨分片合并、全局排名和跨区域查询会变复杂。

惊群与轮询

几十个消费者同时每 10 毫秒查询延迟 ZSet,会在空队列和任务到期瞬间制造突发流量。应使用:

  • 随机抖动。
  • 空队列指数退避。
  • 每次限制领取数量。
  • 消费者并发上限。
  • 通知与轮询结合,但通知不能作为唯一可靠信号。

重试

客户端超时时,命令可能:

  1. 根本没到服务器。
  2. 已到服务器但尚未执行。
  3. 已执行,只是响应丢失。

因此对 ZINCRBY、任务领取、ACK 等写操作盲目重试可能产生重复副作用。

7.3 高可用

Redis 主从复制默认是异步的。主节点刚确认的写入可能还未到达副本;此时发生故障转移,最近的 Set、排行积分或任务状态可能丢失。WAIT 可以降低概率,但不能把 Redis 变成强一致 CP 系统。(Redis)

需要特别考虑:

  • 从副本读排行榜可能看到旧名次。
  • 撤销权限后,从副本读可能短时间仍显示有权限。
  • 任务刚从 ready 移到 processing 时发生切换,可能重新出现或消失。
  • 消费者收到超时后不能确定领取是否成功。
  • 故障恢复后租约回收可能造成重复执行。

因此:

  • 权限等高风险判断应谨慎使用副本读。
  • 延迟任务处理器必须幂等。
  • 关键结算应回到数据库或权威账本复核。
  • 持久化、复制和业务幂等应共同设计,不能把“部署了副本”理解为“不会丢数据”。

Redis Cluster 中,多 Key 命令和 Lua 脚本通常要求相关 Key 位于同一 Hash Slot,应在建模阶段使用 Hash Tag,而不是出现 CROSSSLOT 后临时修改。(Redis)


8. 常见错误与生产事故

8.1 在核心接口中对百万级集合执行 SINTER

  • 现象:Redis 延迟突增,其他 GET、SET 也变慢,慢日志出现集合命令。
  • 根因:共同关注接口直接对多个大集合求交;结果虽少,候选扫描量很大。
  • 排查方法:检查 SLOWLOG、集合基数、命令耗时、CPU 和客户端输出大小。
  • 修复方案:限制关系数量,改为离线预计算,或从最小候选集分批在应用侧验证。
  • 预防措施:为集合大小设置监控阈值,对复杂度高的命令制定准入规则。

8.2 排行榜同分顺序与产品规则不一致

  • 现象:相同积分用户的排名看似随机,或正序、倒序结果相反。
  • 根因:产品要求“先达到该分数者优先”,代码却依赖 member 字典序。
  • 排查方法:检查重复 score、member 格式及查询是否使用 REV
  • 修复方案:增加明确的第二排序设计,或在结算数据库中二次排序。
  • 预防措施:排行榜需求评审时明确同分规则、并列名次规则和分页规则。

8.3 毫秒、秒或纳秒时间戳混用

  • 现象:延迟任务提前几十年执行、永不到期,或时间窗口完全失效。
  • 根因:生产者使用秒,消费者按毫秒比较;或将纳秒时间戳直接转为 float64
  • 排查方法:输出 score,检查数量级;对照 time.Now().Unix()UnixMilli()
  • 修复方案:统一使用 Unix 毫秒,并把单位写入变量名和接口文档。
  • 预防措施:增加时间范围校验,例如拒绝超过合理未来窗口的执行时间。

8.4 使用 ZRANGE 后 ZREM 领取任务

  • 现象:同一任务被多个 worker 同时执行。
  • 根因:查询和删除不是一个原子操作。
  • 排查方法:对照多个消费者日志,检查是否读取了同一个 taskID。
  • 修复方案:使用 Lua 原子地从 ready 移到 processing。
  • 预防措施:为任务执行建立唯一业务键和幂等状态机。

8.5 直接用 ZPOPMIN 实现延迟任务

  • 现象:未来任务被提前取出;worker 失败后任务永久消失。
  • 根因ZPOPMIN 只关心最小 score,不判断它是否已经到期,而且弹出即删除。
  • 排查方法:比较弹出 score 与当前时间,检查失败任务是否存在 processing 记录。
  • 修复方案:先按 score 过滤到期任务,再原子转移到 processing,并设置租约。
  • 预防措施:区分“优先级队列”和“按时间到期的调度队列”。

8.6 GEO 经纬度写反

  • 现象:成员出现在其他国家,查询不到附近门店,或 GEOADD 报坐标越界。
  • 根因:业务对象通常写成纬度、经度,而 Redis 参数顺序是经度、纬度。
  • 排查方法:使用 GEOPOS 检查实际坐标,验证经度绝对值和纬度范围。
  • 修复方案:定义明确的 LongitudeLatitude 字段,禁止使用模糊的 xy
  • 预防措施:增加城市边界校验和固定坐标测试用例。

8.7 Redis Cluster 出现 CROSSSLOT

  • 现象:单 Key 命令正常,SINTER 或延迟任务 Lua 脚本报跨 Slot 错误。
  • 根因:相关 Key 没有使用相同 Hash Tag。
  • 排查方法:使用 CLUSTER KEYSLOT 检查 Key 的 Slot。
  • 修复方案:设计为 follow:{user-group}:afollow:{user-group}:b,或在应用层完成跨分片合并。
  • 预防措施:在进入 Cluster 前完成多 Key 访问模式审计。

8.8 故障转移后任务重复执行

  • 现象:Redis 主从切换后,少量订单通知或定时任务重复处理。
  • 根因:异步复制、领取响应丢失、processing 状态回退或租约重放。
  • 排查方法:对照故障时间、复制偏移、任务状态及业务幂等记录。
  • 修复方案:业务数据库增加唯一执行记录;重复请求返回已有结果。
  • 预防措施:以“可能重复”为正常故障模型进行演练,而不是依赖恰好一次假设。

9. 方案选型与权衡

需求SetSorted SetGEO数据库/专业系统
精确去重可以但浪费 score不适合唯一索引更持久
成员关系判断可用但成本较高不适合复杂关系可用图数据库
交并差原生支持支持部分聚合命令不适合SQL 灵活但延迟较高
排名与范围不支持顺序仅地理范围数据库适合离线结算
优先级队列不适合适合简单模型不适合专用 MQ 可靠性机制更完整
延迟任务不适合可实现,但需补可靠性不适合专用调度系统更成熟
附近位置不适合可自行编码但复杂优于简单附近查询PostGIS/Redis Search 更强
强一致结算不适合作为唯一账本不适合作为唯一账本不适合事务数据库
超大集合分析在线风险较高在线风险较高区域过大风险较高数仓、搜索或离线计算

选型原则不是“Redis 能实现就使用 Redis”,而是先确定:

  1. 数据是否必须精确和持久。
  2. 单 Key 最大规模。
  3. 是否需要跨 Key 运算。
  4. 是否允许最终一致。
  5. 是否接受重复执行。
  6. 故障后如何恢复和审计。
  7. Redis 是权威数据源还是可重建索引。

10. 高频面试题

10.1 Set 适合解决什么问题?

问题

Redis Set 的核心特点和典型场景是什么?

推荐回答

简洁回答:Set 是无序且成员唯一的字符串集合,适合去重、成员判断、标签、权限和集合关系运算。

详细展开:SADD 自动去重,SISMEMBER 适合成员判断,SINTERSUNIONSDIFF 分别实现交集、并集和差集。它不保证插入顺序,因此不应实现时间线或有序队列。(Redis)

面试官追问

  1. Set 和 Hash 有什么区别?
  2. 抽奖应使用 SRANDMEMBER 还是 SPOP
  3. Set 能否按插入顺序返回?

常见错误回答

“Set 就是 Go 的 map。”该回答忽略了集合运算、Redis 网络模型和内部编码差异。

评分点

  • 初级:知道去重和无序。
  • 中级:能说出交并差和业务建模。
  • 高级:能说明 BigKey、跨 Slot 和一致性风险。

10.2 如何实现共同好友或共同关注?

问题

两个用户的共同关注怎么实现?

推荐回答

简洁回答:为每个用户维护关注 Set,用 SINTER 求交集;但大集合不能无条件在线求交。

详细展开:follow:userAfollow:userB 分别存放被关注用户 ID。集合较小时可直接 SINTER;只需要数量时可考虑 SINTERCARD。大规模社交关系应限制集合规模、预计算共同关系,或从较小集合取候选后分批判断。

面试官追问

  1. 两个集合都达到千万级怎么办?
  2. Redis Cluster 下能直接求交吗?
  3. 如何只返回前 20 个共同关注?

常见错误回答

SINTER 是 Redis 原生命令,所以一定很快。”

评分点

  • 初级:能给出 SINTER
  • 中级:知道复杂度和大结果风险。
  • 高级:能提出预计算、分片、候选裁剪和 Cluster 同槽方案。

10.3 为什么 O(N) 命令也可能造成严重阻塞?

问题

SUNION 是 O(N),为什么仍然危险?

推荐回答

简洁回答:O(N) 只说明增长趋势,不说明 N 的大小,也不包含结果分配、协议编码、网络发送和客户端解析成本。

详细展开:百万级输入下,线性扫描本身就很重;Redis 还要构造结果并发送。命令执行占用时间变长时,同一节点上其他命令也会排队,因此一个低频慢命令可能拖慢大量高频 GET。

面试官追问

  1. 客户端设置 50 毫秒超时能否解决?
  2. 使用 Pipeline 是否会更快?
  3. SUNIONSTORE 有何不同?

常见错误回答

“O(N) 已经是最优复杂度,所以没有优化空间。”

评分点

  • 初级:知道 N 大会慢。
  • 中级:能分析 CPU、内存和网络。
  • 高级:能说明服务端继续执行、输出缓冲区和隔离策略。

10.4 Set 的底层只有 intset 和 hashtable 吗?

问题

Redis Set 的底层编码有哪些?

推荐回答

简洁回答:旧资料常概括为 intset 和 hashtable;当前 Redis 8.8 还包含 listpack Set 编码。

详细展开:整数小集合可用 intset;成员少且短时可使用 listpack;规模增长或不再满足紧凑条件时使用哈希表。紧凑结构节省内存并改善局部性,哈希表则提供更好的大规模随机访问。(GitHub)

面试官追问

  1. 为什么紧凑编码可能比哈希表更快?
  2. intset 插入为什么可能不是 O(1)?
  3. 如何查看对象编码?

常见错误回答

“所有 Redis 版本都只有 intset 和 hashtable。”

评分点

  • 初级:知道经典两种编码。
  • 中级:能解释转换条件和内存取舍。
  • 高级:能主动指出版本差异,不机械背旧结论。

10.5 Sorted Set 如何处理重复 member 和重复 score?

问题

ZSet 能不能有重复分数和重复成员?

推荐回答

简洁回答:member 必须唯一,score 可以重复;重复添加同一 member 默认更新 score。

详细展开:Redis 先按 score 排序,相同 score 再按 member 的二进制字典序排序;反向查询时,同分成员使用反向字典序。因此相同积分不等于并列顺序随机,但也不等于按到达时间排序。(Redis)

面试官追问

  1. 同分时先到先得怎么实现?
  2. ZADD NXXX 有什么作用?
  3. 修改 score 后排名如何变化?

常见错误回答

“score 也必须唯一。”

评分点

  • 初级:知道 member 唯一。
  • 中级:知道同分字典序。
  • 高级:能设计第二排序字段并分析精度与查询代价。

10.6 Sorted Set 为什么同时使用字典和跳表?

问题

只用跳表或只用字典不行吗?

推荐回答

简洁回答:字典负责按 member 快速定位,跳表负责按 score 排序、范围查询和排名,两者优化的是不同访问路径。

详细展开:只用字典无法高效获取 Top N 和 score 范围;只用跳表时,仅根据 member 查找旧 score 不够高效。双索引让查询和排序都快,但增加了内存,并要求每次写入同步维护两套结构。(Redis)

面试官追问

  1. ZADD 为什么是 O(log N)
  2. ZSCORE 为什么通常很快?
  3. 双索引会不会数据不一致?

常见错误回答

“字典用于去重,跳表只是为了好看地遍历。”

评分点

  • 初级:能说出字典加跳表。
  • 中级:能对应具体命令。
  • 高级:能说明内存代价、写放大和一致维护。

10.7 为什么 Redis 使用跳表而不是红黑树?

问题

跳表相对平衡树有什么工程优势?

推荐回答

简洁回答:两者都能达到对数级查找,跳表实现更直接,范围遍历和节点前后移动自然,适合 Redis 的排名和范围访问。

详细展开:跳表通过随机层级获得期望 O(log N),无需复杂旋转;底层链表便于范围扫描。它并非理论上全面优于红黑树,而是更符合 Redis 的实现目标。

面试官追问

  1. 跳表最坏复杂度是什么?
  2. 随机层级如何影响内存?
  3. 排名如何做到 O(log N)

常见错误回答

“跳表永远比红黑树快。”

评分点

  • 初级:知道二者都可排序。
  • 中级:知道随机层级和范围遍历。
  • 高级:能从实现复杂度、内存和工作负载说明权衡。

10.8 score 有什么精度问题?

问题

为什么不能直接把纳秒时间戳作为 ZSet score?

推荐回答

简洁回答:score 是 double,只能精确表示 [-2^53, 2^53] 范围内的整数;Unix 纳秒时间戳已超过这个范围。

详细展开:超过精确整数边界后,两个不同整数可能变成相同的浮点表示,导致排序和范围边界异常。毫秒时间戳通常安全;微秒接近但仍在范围内;复合 score 必须计算最大值。(Redis)

面试官追问

  1. 金额能否直接作为 score?
  2. 如何实现时间加序号?
  3. 小数 score 是否完全不能使用?

常见错误回答

“double 有 64 位,所以所有 int64 都能精确表示。”

评分点

  • 初级:知道浮点有精度问题。
  • 中级:记得 2^53 边界。
  • 高级:能结合时间单位、金额缩放和复合编码给出方案。

10.9 如何设计一个实时排行榜?

问题

如何支持加分、Top 100、个人排名和赛季结算?

推荐回答

简洁回答:按赛季建立 ZSet,以 userID 为 member、积分为 score,使用 ZINCRBYZRANGE REVZREVRANK;结算时回数据库复核。

详细展开:Key 应包含游戏、区服和赛季;限制 Top N 查询范围;定义同分规则;为积分事件设置幂等 ID;大规模全服榜可采用分区榜加异步总榜。奖励结算不应只以 Redis 当前值作为唯一账本。

面试官追问

  1. 如何防止重复加分?
  2. 如何处理十亿用户?
  3. 如何实现用户附近排名?

常见错误回答

“所有用户放在一个 ZSet 就完成了。”

评分点

  • 初级:掌握核心命令。
  • 中级:能设计 Key、同分和分页。
  • 高级:能讨论热 Key、分片、结算一致性与容灾。

10.10 如何用 Sorted Set 实现延迟任务?

问题

最小可用方案是什么,生产方案还缺什么?

推荐回答

简洁回答:以执行时间为 score、taskID 为 member;消费者原子地把到期任务从 ready 移到 processing,再执行和 ACK。

详细展开:不能直接 ZRANGEZREM,也不能无条件 ZPOPMIN。生产方案要有领取租约、超时回收、重试次数、死信、幂等、空队列退避和故障恢复。Redis 适合简单调度,但可靠性语义需要业务自行补齐。

面试官追问

  1. worker 处理成功但 ACK 失败怎么办?
  2. 如何避免轮询惊群?
  3. Redis 切主时可能发生什么?

常见错误回答

ZPOPMIN 是原子的,所以能够保证恰好一次。”

评分点

  • 初级:知道时间戳 score。
  • 中级:知道查询删除竞态。
  • 高级:能完整描述租约、回收、幂等和故障模型。

10.11 ZPOPMIN 原子,为什么仍不能保证任务恰好一次?

问题

单命令原子性和业务恰好一次有什么区别?

推荐回答

简洁回答:ZPOPMIN 只保证弹出动作不会被两个客户端同时成功执行,不保证弹出后的业务处理和 ACK 与 Redis 原子提交。

详细展开:弹出后 worker 可能宕机,任务会丢;业务处理成功后响应丢失,客户端可能重试;故障转移也可能使状态回退。恰好一次通常要依赖业务唯一约束、幂等状态机或事务边界。

面试官追问

  1. 至少一次如何实现?
  2. 至多一次如何实现?
  3. 什么场景可以接受 ZPOPMIN

常见错误回答

“Redis 命令是原子的,所以整个消费流程也是原子的。”

评分点

  • 初级:区分弹出和处理。
  • 中级:能解释丢失与重复。
  • 高级:能比较至少一次、至多一次和事实上的恰好一次。

10.12 Redis GEO 的底层是什么?

问题

GEO 是否是一种独立于 Sorted Set 的数据结构?

推荐回答

简洁回答:GEO 的底层存储是 Sorted Set,经纬度编码后写入 score,member 是位置对象 ID。

详细展开:Redis 先用空间编码缩小候选范围,再对候选点计算球面距离。它适合简单的圆形和矩形附近查询,但不支持复杂 GIS 查询;删除 GEO 成员可以使用 ZREM。(Redis)

面试官追问

  1. 为什么要检查相邻网格?
  2. Redis 计算的是道路距离吗?
  3. GEO 查询误差来自哪里?

常见错误回答

“Redis GEO 使用二维 R 树,并能进行精确导航距离计算。”

评分点

  • 初级:知道能查询附近位置。
  • 中级:知道底层是 ZSet 和空间编码。
  • 高级:能说明候选过滤、球面模型、误差和专业 GIS 边界。

10.13 Go 客户端调用这些结构时要注意什么?

问题

go-redis/v9 在并发、超时和错误处理方面有哪些要点?

推荐回答

简洁回答:Client 应全局复用并允许多 goroutine 并发使用;每次调用传 Context 和超时;区分 redis.Nil、超时和真正服务端异常。

详细展开:不要每个请求创建连接;对 ZREVRANK 等“成员不存在”结果使用 errors.Is(err, redis.Nil);对空集合结果不要误判为异常;写命令超时后结果可能未知,重试前必须确认幂等。Cluster 多 Key 操作还要保证同槽。

面试官追问

  1. Context 超时后 Redis 命令一定停止吗?
  2. 哪些命令不存在成员时返回 redis.Nil
  3. Pipeline 是否线程安全?

常见错误回答

“任何非 nil error 都直接重试三次。”

评分点

  • 初级:会创建 Client 和调用命令。
  • 中级:能正确处理 Context 与 redis.Nil
  • 高级:能分析未知执行结果、幂等重试、连接池和 Cluster 路由。

11. 一分钟面试回答

Set 是无序且成员唯一的集合,适合去重、标签、权限和共同关系,常用命令包括 SADDSISMEMBERSINTERSUNIONSDIFF。但交并差会扫描大量成员并生成结果,大集合上可能阻塞 Redis,不能因为是原生命令就默认安全。Sorted Set 在唯一 member 上增加 double score,底层大集合同时维护字典和跳表:字典用于按 member 快速定位,跳表用于排序、范围和排名。相同 score 按 member 的二进制字典序排列,score 精确整数范围只有正负 2^53,所以纳秒时间戳和未经证明的复合分值有风险。排行榜可用 ZINCRBYZRANGE REVZREVRANK;延迟任务以执行时间为 score,但必须用 Lua 原子领取、processing 租约、超时回收和业务幂等,不能宣称恰好一次。GEO 底层也是 Sorted Set,适合简单附近查询,不替代 PostGIS 或完整 GIS。生产上还要考虑 HotKey、BigKey、Cluster 同槽、异步复制和故障转移后的重复或数据丢失。


12. 本章总结

  1. Set 的核心价值是唯一成员、快速成员判断和集合运算,而不是有序遍历。
  2. 集合运算的风险由输入规模、集合数量、结果大小和网络共同决定。
  3. Sorted Set 的排序键实际是 (score, member),同分业务规则必须明确设计。
  4. 字典与跳表分别服务于按成员访问和按顺序访问,双索引以更多内存换取多种高效查询。
  5. double score 不能精确表示所有 int64,时间单位和复合评分必须经过数值边界分析。
  6. Sorted Set 可以实现延迟任务,但调度索引不等于可靠消息系统。
  7. GEO 适合简单附近查询,不能承担道路距离、复杂多边形和高精度测绘。
  8. Redis 的单命令原子性不能扩展成整个业务流程的恰好一次或强一致保证。

13. 自测清单

  1. Set 为什么不能用于需要稳定插入顺序的业务?
  2. 两个集合的交集结果很少,为什么 SINTER 仍可能很慢?
  3. SUNIONSTORE 相比 SUNION 解决了什么,又增加了什么成本?
  4. 当前 Redis 8.8 的 Set 可能采用哪些内部编码?
  5. Sorted Set 中两个成员 score 相同,最终顺序由什么决定?
  6. 为什么 double 不能精确表示全部 int64?
  7. 字典和跳表分别为 Sorted Set 的哪些命令提供支持?
  8. 为什么 ZRANGEZREM 不能安全地领取延迟任务?
  9. 延迟任务的 processing 租约解决了什么问题,仍然无法解决什么问题?
  10. Redis GEO 与 PostGIS 或 Redis Search 的适用边界有什么不同?

14. 官方资料

  • Redis Open Source 下载与当前版本信息。(Redis)
  • Redis Set 数据类型文档。(Redis)
  • SADDSINTERSUNIONSDIFF 命令文档。(Redis)
  • Redis Sorted Set 数据类型文档。(Redis)
  • ZADDZRANGEZRANKZPOPMIN 命令文档。(Redis)
  • Redis GEO 数据类型及 GEOADDGEOSEARCHGEODIST 文档。(Redis)
  • Redis 8.8 t_set.ct_zset.c 官方源码。(GitHub)
  • Redis Lua、Pipeline、复制和 Cluster 规范。(Redis)